{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 2.1 用大模型构建新人答疑机器人\n",
    "\n",
    "## 🚄 前言\n",
    "你在一家教育内容开发公司工作，随着新员工的持续加入，频繁的答疑需求导致了显著的时间与资源成本。你决定使用大模型技术来构建一个答疑机器人，提升答疑的准确率与效率。\n",
    "\n",
    "## 🍁 课程目标\n",
    "学完本节课程后，你将能够：\n",
    "\n",
    "* 通过API调用通义千问\n",
    "* 学习大模型的工作原理\n",
    "* 了解大模型的局限性及其解决方案\n",
    "\n",
    "## ⚠️ 环境准备 \n",
    "为了让你能够更流畅地体验教程内容，建议你首先完成 **阿里云大模型高级工程师ACP认证课程** 中的 [**环境准备**](https://edu.aliyun.com/course/3130200/lesson/343310285) 章节，并确保课程所需的环境已经正确安装。\n",
    "\n",
    "\n",
    "## 💻 1. 通过API调用通义千问\n",
    "\n",
    "体验大模型最直接的方式就是通过网页界面（如[通义千问](https://tongyi.aliyun.com/qianwen/)）与它对话。不过作为开发者，往往需要在自己的应用中集成大模型能力，你可以使用业界广泛采用的 OpenAI Python SDK 来调用通义千问大模型。你已经在`1.0 计算环境准备`安装了必要的依赖，在执行接下来的代码前，确认已经切换到新创建的python环境，如本例中创建的 `Python (llm_learn)`。\n",
    "\n",
    "<a href=\"https://img.alicdn.com/imgextra/i4/O1CN01nf2EYI1pwhhMbOWHe_!!6000000005425-0-tps-2258-1004.jpg\" target=\"_blank\">\n",
    "<img src=\"https://img.alicdn.com/imgextra/i4/O1CN01nf2EYI1pwhhMbOWHe_!!6000000005425-0-tps-2258-1004.jpg\" width=\"600\">\n",
    "</a>\n",
    "\n",
    "<a href=\"https://img.alicdn.com/imgextra/i3/O1CN01rn0jvB1Z1QJXUWaG2_!!6000000003134-0-tps-3138-914.jpg\" target=\"_blank\">\n",
    "<img src=\"https://img.alicdn.com/imgextra/i3/O1CN01rn0jvB1Z1QJXUWaG2_!!6000000003134-0-tps-3138-914.jpg\" width=\"600\">\n",
    "</a>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为了调用通义千问，你需要前往阿里云大模型服务平台百炼，开通 [模型调用服务](https://bailian.console.aliyun.com/#/model-market) 并 [创建一个API Key](https://bailian.console.aliyun.com/?apiKey=1#/api-key)。\n",
    "\n",
    "> 如果页面顶部显示如下，则表示你尚未开通百炼的模型调用服务。开通服务后可以调用模型。\n",
    "> <img src=\"https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/5298748271/p856749.png\" width=\"600\">\n",
    "\n",
    "> 请参考下图中的操作步骤来获取你的 API KEY ：\n",
    ">\n",
    "> <a href=\"https://img.alicdn.com/imgextra/i3/O1CN01hdY7Ln1Xm1QHVrfTA_!!6000000002965-2-tps-3428-1868.png\" target=\"_blank\">\n",
    "> <img src=\"https://img.alicdn.com/imgextra/i3/O1CN01hdY7Ln1Xm1QHVrfTA_!!6000000002965-2-tps-3428-1868.png\" width=\"600\">\n",
    "> </a>\n",
    "\n",
    "在使用 API 之前，你需要妥善处理 API Key 的安全问题。直接在代码中写入 API Key 是一个不好的习惯，因为这样很容易在分享代码时泄露密钥，并且在更换API Key后所有明文编码的API Key部分都需要修改。更安全、便捷的做法是将 API Key 存储在环境变量中。\n",
    "\n",
    "下面的代码会从配置文件中加载你的 API Key，并将其设置为环境变量。代码执行后会显示 API Key 的前五位字符（后面用星号代替），这样你可以确认配置是否正确，同时又不会暴露完整的密钥。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T08:58:03.667571Z",
     "iopub.status.busy": "2024-11-15T08:58:03.667184Z",
     "iopub.status.idle": "2024-11-15T08:59:44.060371Z",
     "shell.execute_reply": "2024-11-15T08:59:44.059650Z",
     "shell.execute_reply.started": "2024-11-15T08:58:03.667545Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 加载百炼的 API Key 用于调用通义千问大模型\n",
    "import os\n",
    "from config.load_key import load_key\n",
    "load_key()\n",
    "print(f'''你配置的 API Key 是：{os.environ[\"DASHSCOPE_API_KEY\"][:5]+\"*\"*5}''')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> - 你需要 “回车” 确认你的输入，输入成功后你会看到```“ 你配置的 API Key 是：sk-88***** ” ```的字样。 \n",
    "> - 如果你需要更换 “API-KEY”，请编辑上一级目录的 “KEY.json” 文件。\n",
    "> - 如果你在使用VS-CODE，API-KEY的输入框会出现在窗体的**顶部**。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "先来尝试一个简单的对话。下面的代码创建了一个名为“公司小蜜”的助手，它可以回答关于公司运营的问题。你可以使用“选择项目管理工具”这个常见问题作为示例："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T08:59:46.180449Z",
     "iopub.status.busy": "2024-11-15T08:59:46.179984Z",
     "iopub.status.idle": "2024-11-15T08:59:59.498380Z",
     "shell.execute_reply": "2024-11-15T08:59:59.497544Z",
     "shell.execute_reply.started": "2024-11-15T08:59:46.180423Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "from openai import OpenAI\n",
    "import os\n",
    "client = OpenAI(\n",
    "    api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "    base_url=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    ")\n",
    "def get_qwen_response(prompt):\n",
    "    response = client.chat.completions.create(\n",
    "        model=\"qwen-max\",\n",
    "        messages=[\n",
    "            # system message 用于设置大模型的角色和任务\n",
    "            {\"role\": \"system\", \"content\": \"你负责教育内容开发公司的答疑，你的名字叫公司小蜜，你要回答同事们的问题。\"},\n",
    "            # user message 用于输入用户的问题\n",
    "            {\"role\": \"user\", \"content\": prompt}\n",
    "        ]\n",
    "    )\n",
    "    return response.choices[0].message.content\n",
    "response = get_qwen_response(\"我们公司项目管理应该用什么工具\")\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> 如果你想实现多轮对话（让大模型参考历史对话信息进行回复），可以参考[多轮对话](https://help.aliyun.com/zh/model-studio/multi-round-conversation)。\n",
    "\n",
    "运行上面的代码后，你会注意到需要等待一段时间（约20秒）才能看到完整的回复。这是因为默认情况下，API 会等待模型生成完所有内容后才一次性返回结果。在实际应用中，这种等待可能会影响用户体验 —— 想象一下用户盯着一个空白界面等待20秒的场景！\n",
    "\n",
    "幸运的是，你可以使用\"流式输出\"（Streaming）来优化这个问题。使用流式输出时，模型会像人类打字一样，一边思考一边输出，让用户能够立即看到部分回复，大大提升交互体验。接下来，看看如何实现流式输出...\n",
    "> 💡 小贴士：流式输出只是改变了内容的展示方式，模型的思考过程和最终答案的质量都保持不变。你可以放心使用这个功能来优化你的应用体验。\n",
    "\n",
    "要实现流式输出，只需在之前的代码基础上添加 `stream=True` 参数，并调整输出方式："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T09:00:04.752657Z",
     "iopub.status.busy": "2024-11-15T09:00:04.752276Z",
     "iopub.status.idle": "2024-11-15T09:00:18.769987Z",
     "shell.execute_reply": "2024-11-15T09:00:18.769374Z",
     "shell.execute_reply.started": "2024-11-15T09:00:04.752633Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "def get_qwen_stream_response(user_prompt,system_prompt):\n",
    "    response = client.chat.completions.create(\n",
    "        model=\"qwen-max\",\n",
    "        messages=[\n",
    "            {\"role\": \"system\", \"content\": system_prompt},\n",
    "            {\"role\": \"user\", \"content\": user_prompt}\n",
    "        ],\n",
    "        stream=True\n",
    "    )\n",
    "    for chunk in response:\n",
    "        yield chunk.choices[0].delta.content\n",
    "\n",
    "response = get_qwen_stream_response(user_prompt=\"我们公司项目管理应该用什么工具\",system_prompt=\"你负责教育内容开发公司的答疑，你的名字叫公司小蜜，你要回答同事们的问题。\")\n",
    "for chunk in response:\n",
    "    print(chunk, end=\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "通过对通义千问模型的两次提问，你可能会发现一些有趣的现象：\n",
    "1. 即使问题完全相同，每次的回答都略有不同。这是大模型的特性之一，就像人类一样，它会用不同的方式表达相似的观点。\n",
    "2. 模型给出的建议集中在一些通用的项目管理工具上，比如 Jira、Trello 或 Asana。虽然大模型知识丰富，但它并不了解你公司的具体情况，比如已有的工具链、团队规模、预算限制等。\n",
    "> 公司使用的项目管理软件资料可以从docs/内容开发工程师岗位指导说明书.pdf文件中找到。\n",
    "\n",
    "这两个现象其实很有意思！为什么大模型会表现出这样的特点呢？要理解这一点，需要掀开大模型的“神秘面纱”，看看它是如何思考和工作的。别担心，你将通过简单直观的方式来理解这些概念。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 📚 2. 大模型是如何工作的\n",
    "近几十年来，人工智能经历了从基础算法到生成式AI的深刻演变。生成式AI通过学习大量数据可以创造出全新的内容，如文本、图像、音频和视频，这极大地推动了AI技术的广泛应用。常见的应用场景包括智能问答（如通义千问、GPT）、创意作画（如Stable Diffusion）以及代码生成（如通义灵码）等，涵盖了各个领域，让AI触手可及。\n",
    "\n",
    "<a href=\"https://img.alicdn.com/imgextra/i3/O1CN01XhNFzh1bj3EybLhgk_!!6000000003500-0-tps-2090-1138.jpg\" target=\"_blank\">\n",
    "<img src=\"https://img.alicdn.com/imgextra/i3/O1CN01XhNFzh1bj3EybLhgk_!!6000000003500-0-tps-2090-1138.jpg\" width=\"600\">\n",
    "</a>\n",
    "\n",
    "智能问答作为大模型最经典且广泛的应用之一，是我们探索大模型工作机制的最佳范例。接下来将介绍大模型在问答场景中的工作流程，帮助你更深入地理解其背后的技术原理。\n",
    "\n",
    "### 2.1. 大模型的问答工作流程\n",
    "下面以“ACP is a very”为输入文本向大模型发起一个提问，下图展示从发起提问到输出文本的完整流程。\n",
    "\n",
    "<a href=\"https://img.alicdn.com/imgextra/i1/O1CN01yLBhyu1gSAlt3oI0p_!!6000000004140-2-tps-2212-1070.png\" target=\"_blank\">\n",
    "<img src=\"https://img.alicdn.com/imgextra/i1/O1CN01yLBhyu1gSAlt3oI0p_!!6000000004140-2-tps-2212-1070.png\" width=\"800\">\n",
    "</a>\n",
    "\n",
    "大模型的问答工作流程有以下五个阶段：\n",
    "\n",
    "**第一阶段：输入文本分词化**\n",
    "\n",
    "分词（Token）是大模型处理文本的基本单元，通常是词语、词组或者符号。我们需要将“ACP is a very”这个句子分割成更小且具有独立语义的词语（Token），并且为每个Token分配一个ID。如果您对通义千问的tokenizer细节感兴趣，请参考：[Tokenization](https://github.com/QwenLM/Qwen/blob/main/tokenization_note_zh.md)。\n",
    "\n",
    "<a href=\"https://gw.alicdn.com/imgextra/i1/O1CN019gAS3k1DrhwpIdHl6_!!6000000000270-0-tps-2414-546.jpg\" target=\"_blank\">\n",
    "<img src=\"https://gw.alicdn.com/imgextra/i1/O1CN019gAS3k1DrhwpIdHl6_!!6000000000270-0-tps-2414-546.jpg\" width=\"800\">\n",
    "</a>\n",
    "\n",
    "**第二阶段：Token向量化**\n",
    "\n",
    "计算机只能理解数字，无法直接理解Token的含义。因此需要将Token进行数字化转换（即转化为向量），使其可以被计算机所理解。Token向量化会将每个Token转化为固定维度的向量。\n",
    "\n",
    "\n",
    "**第三阶段：大模型推理**\n",
    "\n",
    "大模型通过大量已有的训练数据来学习知识，当我们输入新内容，比如“ACP is a very”时，大模型会结合所学知识进行推测。它会计算所有可能Token的概率，得到候选Token的概率集合。最后，大模型通过计算选出一个Token作为下一个输出。\n",
    "\n",
    "这就解释了为什么当询问公司的项目管理工具时，模型无法提供内部工具的建议，这是因为其推测能力是基于已有的训练数据，对它未接触的知识无法给出准确的回答。因此，在需要答疑机器人回答私域知识时，需要针对性地解决这一问题，在本小节第3部分会进一步阐述。\n",
    "\n",
    "**第四阶段：输出Token**\n",
    "\n",
    "由于大模型会根据候选Token的概率进行随机挑选，这就会导致“即使问题完全相同，每次的回答都略有不同”。为了控制生成内容的随机性，目前普遍是通过temperature和top_p来调整的。\n",
    "\n",
    "例如，下图中大模型输出的第一个候选Token集合为“informative（50%）”、“fun（25%）”、“enlightening（20%）”、“boring（5%）”。通过调整temperature或top_p参数，将影响大模型在候选Token集合中的选择倾向，如选择概率最高的“informative”。你可以在本小节 2.2 中进一步了解这两个参数。\n",
    "\n",
    "<a href=\"https://img.alicdn.com/imgextra/i3/O1CN01N93ZE81e6zAZA4TiK_!!6000000003823-0-tps-582-340.jpg\" target=\"_blank\">\n",
    "<img src=\"https://img.alicdn.com/imgextra/i3/O1CN01N93ZE81e6zAZA4TiK_!!6000000003823-0-tps-582-340.jpg\" width=\"180\">\n",
    "</a>\n",
    "\n",
    "特别地，“informative”会被继续送入大模型，用于生成候选Token。这个过程被称为自回归，它会利用到输入文本和已生成文本的信息。大模型采用这种方法依次生成候选Token。\n",
    "\n",
    "**第五阶段：输出文本**\n",
    "\n",
    "循环第三阶段和第四阶段的过程，直到输出特殊Token（如<EOS>，end of sentence，即“句子结束”标记）或输出长度达到阈值，从而结束本次问答。大模型会将所有生成的内容输出。当然你可以使用大模型的流式输出能力，即预测一些Token立即进行返回。这个例子最终会输出“ACP is a very informative course.”。\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.2 影响大模型内容生成的随机性参数\n",
    "\n",
    "假设在一个对话问答场景中，用户提问为：“在大模型ACP课程中，你可以学习什么？”。为了模拟大模型生成内容的过程，我们预设了一个候选Token集合，这些Token分别为：“RAG”、“提示词”、“模型”、“写作”、“画画”。大模型会从这5个候选Token中选择一个作为结果输出（next-token），如下所示。\n",
    "> 用户提问：在大模型ACP课程中，你可以学习什么？<br><br>\n",
    "> 大模型回答：RAG <br>\n",
    "\n",
    "在这个过程中，有两个重要参数会影响大模型的输出：temperature 和 top_p，它们用来控制大模型生成内容的随机性和多样性。下面介绍这两个参数的工作原理和使用方式。\n",
    "\n",
    "#### 2.2.1 temperature：调整候选Token集合的概率分布\n",
    "\n",
    "在大模型生成下一个词（next-token）之前，它会先为候选Token计算一个初始概率分布。这个分布表示每个候选Token作为next-token的概率。temperature是一个调节器，它通过改变候选Token的概率分布，影响大模型的内容生成。通过调节这个参数，你可以灵活地控制生成文本的多样性和创造性。\n",
    "\n",
    "为了更直观地理解，下图展示了不同temperature值对候选Token概率分布的影响。绘图代码放置在 /resources/2_1 文件目录下。\n",
    "\n",
    "<a href=\"https://img.alicdn.com/imgextra/i4/O1CN0137QeqL1o3uhFmaXHU_!!6000000005170-0-tps-3538-1242.jpg\" target=\"_blank\">\n",
    "<img src=\"https://img.alicdn.com/imgextra/i4/O1CN0137QeqL1o3uhFmaXHU_!!6000000005170-0-tps-3538-1242.jpg\" width=\"1000\">\n",
    "</a>\n",
    "\n",
    "图中的低、中、高温度基于通义千问Max模型的范围[0, 2)划分。\n",
    "\n",
    "由上图可知，温度从低到高（0.1 -> 0.7 -> 1.2），概率分布从陡峭趋于平滑，候选Token“RAG”从出现的概率从0.8 -> 0.6 -> 0.3，虽然依然是出现概率最高的，但是已经和其它的候选Token概率接近了，最终输出也会从相对固定到逐渐多样化。\n",
    "\n",
    "针对不同使用场景，可参考以下建议设置 temperature 参数：\n",
    "- 明确答案（如生成代码）：调低温度。\n",
    "- 创意多样（如广告文案）：调高温度。\n",
    "- 无特殊需求：使用默认温度（通常为中温度范围）。\n",
    "\n",
    "需要注意的是，当 temperature=0 时，虽然会最大限度降低随机性，但无法保证每次输出完全一致。如果想深入了解，可查阅 [temperature的底层算法实现](https://github.com/huggingface/transformers/blob/v4.49.0/src/transformers/generation/logits_process.py#L226)。\n",
    "\n",
    "下面体验temperature的效果。通过调整temperature值，对同一问题提问 10 次，观察回答内容的波动情况。\n",
    "> temperature的示例代码跟接下来要讲解的top_p类似，先封装以供后续使用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2024-11-15T12:06:39.470715Z",
     "iopub.status.busy": "2024-11-15T12:06:39.470351Z",
     "iopub.status.idle": "2024-11-15T12:06:44.434877Z",
     "shell.execute_reply": "2024-11-15T12:06:44.434229Z",
     "shell.execute_reply.started": "2024-11-15T12:06:39.470689Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "def get_qwen_stream_response(user_prompt, system_prompt, temperature, top_p):\n",
    "    response = client.chat.completions.create(\n",
    "        model=\"qwen-max\",\n",
    "        messages=[\n",
    "            {\"role\": \"system\", \"content\": system_prompt},\n",
    "            {\"role\": \"user\", \"content\": user_prompt}\n",
    "        ],\n",
    "        temperature=temperature,\n",
    "        top_p=top_p,\n",
    "        stream=True\n",
    "    )\n",
    "    \n",
    "    for chunk in response:\n",
    "        yield chunk.choices[0].delta.content\n",
    "\n",
    "# temperature，top_p的默认值使用通义千问Max模型的默认值\n",
    "def print_qwen_stream_response(user_prompt, system_prompt, temperature=0.7, top_p=0.8, iterations=10):\n",
    "    for i in range(iterations):\n",
    "        print(f\"输出 {i + 1} : \", end=\"\")\n",
    "        ## 防止限流，添加延迟\n",
    "        time.sleep(0.5)\n",
    "        response = get_qwen_stream_response(user_prompt, system_prompt, temperature, top_p)\n",
    "        output_content = ''\n",
    "        for chunk in response:\n",
    "            output_content += chunk\n",
    "        print(output_content)\n",
    "\n",
    "# 通义千问Max模型：temperature的取值范围是[0, 2)，默认值为0.7\n",
    "# 设置temperature=0\n",
    "print_qwen_stream_response(user_prompt=\"马也可以叫做\", system_prompt=\"请帮我续写内容，字数要求是4个汉字以内。\", temperature=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T12:06:45.417531Z",
     "iopub.status.busy": "2024-11-15T12:06:45.417146Z",
     "iopub.status.idle": "2024-11-15T12:06:50.629069Z",
     "shell.execute_reply": "2024-11-15T12:06:50.628320Z",
     "shell.execute_reply.started": "2024-11-15T12:06:45.417506Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 设置temperature=1.9\n",
    "print_qwen_stream_response(user_prompt=\"马也可以叫做\", system_prompt=\"请帮我续写内容，字数要求是4个汉字以内。\", temperature=1.9)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "jupyter": {
     "outputs_hidden": true
    },
    "tags": []
   },
   "source": [
    "从实验中可以明显观察到，温度值越高，模型生成的内容更具变化和多样性。\n",
    "\n",
    "#### 2.2.2 top_p：控制候选Token集合的采样范围\n",
    "\n",
    "top_p 是一种筛选机制，用于从候选 Token 集合中选出符合特定条件的“小集合”。具体方法是：按概率从高到低排序，选取累计概率达到设定阈值的 Token 组成新的候选集合，从而缩小选择范围。\n",
    "\n",
    "下图展示了不同top_p值对候选Token集合的采样效果。\n",
    "\n",
    "<a href=\"https://img.alicdn.com/imgextra/i1/O1CN01xmkonv21sNL6VtQpi_!!6000000007040-0-tps-2732-1282.jpg\" target=\"_blank\">\n",
    "<img src=\"https://img.alicdn.com/imgextra/i1/O1CN01xmkonv21sNL6VtQpi_!!6000000007040-0-tps-2732-1282.jpg\" width=\"700\">\n",
    "</a>\n",
    "\n",
    "图示中蓝色部分表示累计概率达到top_p阈值（如0.5或0.8）的Token，它们组成新的候选集合；灰色部分则是未被选中的Token。\n",
    "\n",
    "当top_p=0.5时，模型优先选择最高概率的Token，即“RAG”；而当top_p=0.8时，模型会在“RAG”、“提示词”、“模型”这三个Token中随机选择一个生成输出。\n",
    "\n",
    "\n",
    "由此可见，top_p值对大模型生成内容的影响可总结为：\n",
    "- 值越大 ：候选范围越广，内容更多样化，适合创意写作、诗歌生成等场景。\n",
    "- 值越小 ：候选范围越窄，输出更稳定，适合新闻初稿、代码生成等需要明确答案的场景。\n",
    "- 极小值（如 0.0001）：理论上模型只选择概率最高的 Token，输出非常稳定。但实际上，由于分布式系统、模型输出的额外调整等因素可能引入的微小随机性，仍无法保证每次输出完全一致。\n",
    "\n",
    "\n",
    "下面体验top_p的效果。通过调整top_p值，对同一问题提问10次，观察回答内容的波动情况。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T12:06:52.894507Z",
     "iopub.status.busy": "2024-11-15T12:06:52.894139Z",
     "iopub.status.idle": "2024-11-15T12:06:58.171739Z",
     "shell.execute_reply": "2024-11-15T12:06:58.171137Z",
     "shell.execute_reply.started": "2024-11-15T12:06:52.894481Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 通义千问Max模型：top_p取值范围为(0,1]，默认值为0.8。\n",
    "# 设置top_p=0.001\n",
    "print_qwen_stream_response(user_prompt=\"为一款智能游戏手机取名，可以是\", system_prompt=\"请帮我取名，字数要求是4个汉字以内。\", top_p=0.001)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T12:06:58.394593Z",
     "iopub.status.busy": "2024-11-15T12:06:58.394205Z",
     "iopub.status.idle": "2024-11-15T12:07:03.378361Z",
     "shell.execute_reply": "2024-11-15T12:07:03.377556Z",
     "shell.execute_reply.started": "2024-11-15T12:06:58.394567Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# 设置top_p=0.8\n",
    "print_qwen_stream_response(user_prompt=\"为一款智能游戏手机取名，可以是\",system_prompt=\"请帮我取名，字数要求是4个汉字以内。\", top_p=0.8)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "根据实验结果可以发现，top_p值越高，大模型的输出结果随机性越高。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.2.3 小结 \n",
    "\n",
    "**是否需要同时调整temperature和top_p？**\n",
    "\n",
    "为了确保生成内容的可控性，建议不要同时调整top_p和temperature，同时调整可能导致输出结果不可预测。你可以优先调整其中一种参数，观察其对结果的影响，再逐步微调。\n",
    "\n",
    "><br>**知识延展：top_k**<br>\n",
    "在通义千问系列模型中，参数top_k也有类似top_p的能力，可查阅[通义千问API文档](https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api?spm=a2c4g.11186623.help-menu-2400256.d_3_3_0.68332bdb2Afk2s&scm=20140722.H_2712576._.OR_help-V_1)。它是一种采样机制，从概率排名前k的Token中随机选择一个进行输出。一般来说，top_k越大，生成内容越多样化；top_k越小，内容则更固定。当top_k设置为1时，模型仅选择概率最高的Token，输出会更加稳定，但也会导致缺乏变化和创意。<br><br>\n",
    ">**知识延展：seed**<br>\n",
    "在通义千问系列模型中，参数seed也支持控制生成内容的确定性，可查阅[通义千问API文档](https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api?spm=a2c4g.11186623.help-menu-2400256.d_3_3_0.68332bdb2Afk2s&scm=20140722.H_2712576._.OR_help-V_1)。在每次模型调用时传入相同的seed值，并保持其他参数不变，模型会尽最大可能返回相同结果，但无法保证每次结果完全一致。<br><br>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**设置 temperature、top_p、seed 控制大模型输出，为何仍存在随机性？**\n",
    "\n",
    "即使将 temperature 设置为 0、top_p 设置为极小值（如 0.0001），并使用相同的 seed ，同一个问题的生成结果仍可能出现不一致。这是因为一些复杂因素可能引入微小的随机性，例如大模型运行在分布式系统中，或模型输出引入了优化。\n",
    "\n",
    "举个例子：\n",
    "分布式系统就像用不同的机器切面包。虽然每台机器都按照相同的设置操作，但由于设备之间的细微差异，切出来的面包片可能还是会略有不同。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ⚙️ 3. 让大模型能够回答私域知识问题\n",
    "\n",
    "回到最初的挑战：答疑机器人无法回答“我们公司项目管理用什么工具”这类内部问题。根本原因在于，大模型的知识来源于其训练数据，这些数据通常是公开的互联网信息，**不包含**任何特定公司的内部文档、政策或流程。\n",
    "\n",
    "你可以把大模型想象成一台刚出厂的超级计算机：它的 CPU（**推理能力**）极其强大，硬盘（**模型权重**）里也预装了海量的通用知识。但对于你公司的“内部资料”，它的硬盘里是空白的。\n",
    "\n",
    "面对这个问题，最直观的解决思路就是：在运行时，把公司的内部知识**临时**告诉它。\n",
    "\n",
    "### 3.1 初步方案：在提示词中“喂”入知识\n",
    "\n",
    "你可以来验证这个思路：将公司项目管理工具的说明文档，直接添加到给模型的指令（System Prompt）中，作为背景知识提供给它。\n",
    "> 💡 公司使用的项目管理软件资料可以从docs/内容开发工程师岗位指导说明书.pdf文件中找到。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "user_question = \"我是软件一组的，请问项目管理应该用什么工具\"\n",
    "\n",
    "knowledge = \"\"\"公司项目管理工具有两种选择：\n",
    "  1. **Jira**：对于软件开发团队来说，Jira 是一个非常强大的工具，支持敏捷开发方法，如Scrum和Kanban。它提供了丰富的功能，包括问题跟踪、时间跟踪等。\n",
    "\n",
    "  2. **Microsoft Project**：对于大型企业或复杂项目，Microsoft Project 提供了详细的计划制定、资源分配和成本控制等功能。它更适合那些需要严格控制项目时间和成本的场景。\n",
    "  \n",
    "  在一般情况下请使用Microsoft Project，公司购买了完整的许可证。软件研发一组、三组和四组正在使用Jira，计划于2026年之前逐步切换至Microsoft Project。\n",
    "\"\"\"\n",
    "\n",
    "response = get_qwen_stream_response(\n",
    "    user_prompt=user_question,\n",
    "    # 将公司项目管理工具相关的知识作为背景信息传入系统提示词\n",
    "    system_prompt=\"你负责教育内容开发公司的答疑，你的名字叫公司小蜜，你要回答学员的问题。\"+ knowledge,\n",
    "    temperature=0.7,\n",
    "    top_p=0.8\n",
    ")\n",
    "\n",
    "for chunk in response:\n",
    "    print(chunk, end=\"\")\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "实验成功了！在提示词中加入相关的背景知识，大模型确实能够准确回答关于公司内部工具的问题。这个发现令人振奋，似乎你已经找到了解决私域知识问答的钥匙。\n",
    "\n",
    "然而，当你试图将更多的公司文档（例如几十页的员工手册、上百页的技术规范）都用这种方式“喂”给大模型时，一个新的、更严峻的挑战出现了。\n",
    "\n",
    "### 3.2 核心瓶颈：有限的上下文窗口\n",
    "<table>\n",
    "<tr>\n",
    "<td style=\"width: 70%; vertical-align: top; border: none;\">\n",
    "  <p>大模型接收我们输入（包括指令、问题和背景知识）的地方，被称为<b>上下文窗口（Context Window）</b>。你可以把它理解为计算机的“内存（RAM）”——它的容量是有限的。</p>\n",
    "  <p>你无法将整个公司的知识库（成百上千份文档）一次性塞进这个有限的窗口里。一旦输入内容超过模型的最大限制，就会导致错误。</p>\n",
    "  <p>这引出了一个核心问题：你需要对放入上下文窗口的内容进行<b>筛选和管理</b>。</p>\n",
    "</td>\n",
    "<td style=\"width: 30%; border: none;\">\n",
    "  <img src=\"https://img.alicdn.com/imgextra/i2/O1CN01EzBbZb1hYsCMyXCWi_!!6000000004290-2-tps-1536-1024.png\">\n",
    "</td>\n",
    "</tr>\n",
    "</table>\n",
    "\n",
    "\n",
    "\n",
    "### 3.3 解决之道：上下文工程 (Context Engineering)\n",
    "\n",
    "简单粗暴地将信息塞进上下文，除了会超出窗口限制外，还会带来一系列“隐性”问题：\n",
    "\n",
    "1.  **效率低**：上下文越长，大模型处理所需的时间就越长，导致用户等待时间增加。\n",
    "2.  **成本高**：大部分模型是按输入和输出的文本量计费的，冗长的上下文意味着更高的成本。\n",
    "3.  **信息干扰**：如果上下文中包含了大量与当前问题无关的信息，就像在开卷考试时给了考生一本错误科目的教科书，反而会干扰模型的判断，导致回答质量下降。\n",
    "\n",
    "你一定会意识到，成功的关键**不在于“喂”给模型多少知识，而在于“喂”得有多准**。\n",
    "\n",
    "如何在正确的时间，将最相关、最精准的知识，动态地加载到大模型有限的上下文窗口中？——这门系统性地设计、构建和优化上下文的实践，就是**上下文工程（Context Engineering）**。\n",
    "\n",
    "从这个角度看，**许多大模型应用的失败，并非模型本身不够智能，而是“上下文”的失败**。上下文工程正是释放大模型潜力的关键所在。\n",
    "\n",
    "那么，上下文工程具体包含哪些技术呢？你可以通过下面这张概念图来快速了解它的核心版图：\n",
    "\n",
    "<table style=\"width: 100%;\">\n",
    "<tr>\n",
    "  \n",
    "  <td style=\"width: 70%; vertical-align: top; border: none; padding-right: 20px;\">\n",
    "    <h4>💡 上下文工程 (Context Engineering) 的核心技术</h4>\n",
    "    <p>它是构建可靠、高效大模型应用的一系列关键技术的总和，主要包括：</p>\n",
    "    <ul>\n",
    "      <li><strong>RAG (检索增强生成)</strong>：从外部知识库（如公司文档）中<strong>检索</strong>信息，为模型提供精准的回答依据。</li>\n",
    "      <li><strong>Prompt (提示词工程)</strong>：通过精心设计的<strong>指令</strong>，精确地引导模型的思考方式和输出格式。</li>\n",
    "      <li><strong>Tool (工具使用)</strong>：赋予模型调用外部<strong>工具</strong>（如计算器、搜索引擎、API）的能力，以获取实时信息或执行特定任务。</li>\n",
    "      <li><strong>Memory (记忆机制)</strong>：为模型建立长短期<strong>记忆</strong>，使其能够在连续对话中理解历史上下文。</li>\n",
    "    </ul>\n",
    "  </td>\n",
    "  \n",
    "  <td style=\"width: 30%; vertical-align: top; border: none;\">\n",
    "    <img src=\"https://img.alicdn.com/imgextra/i3/O1CN01fwRWvW1Rn2b9NdQTW_!!6000000002155-2-tps-1024-1024.png\" alt=\"Context Engineering Diagram\" style=\"width: 100%; max-width: 400px;\">\n",
    "  </td>\n",
    "</tr>\n",
    "</table>\n",
    "\n",
    "\n",
    "你可能已经注意到，这里出现了一些新名词。别担心，你不需要立刻掌握所有细节。这里展示这张图的目的是为了让你建立一个整体认知：**接下来要学习的几个知识，都是上下文工程的一部分**。\n",
    "\n",
    "为了让你更容易理解和吸收，在后续的课程中，这些概念将作为独立的主题，逐一详细拆解和实践。\n",
    "\n",
    "现在，你需要聚焦于当前最紧迫的问题——如何解决私域知识问答。在这个技术版图中，**RAG** 是最直接、最有效的解决方案。接下来，你将深入学习它。\n",
    "\n",
    "\n",
    "### 3.4 技术方案：RAG（检索增强生成）\n",
    "\n",
    "**RAG（Retrieval-Augmented Generation，检索增强生成）** 就是实现上下文工程的强大技术方案。它的核心思想是：\n",
    "\n",
    "在用户提问时，不再将全部知识库硬塞给大模型，而是先**自动检索**出与问题最相关的私有知识片段，然后将这些精准的片段与用户问题合并后，一同传给大模型，从而**生成**最终的答案。这样既避免了提示词过长的问题，又能确保大模型获得相关的背景信息。\n",
    "\n",
    "构建一个 RAG 应用通常会分为两个阶段\n",
    "\n",
    "#### 第一阶段：建立索引\n",
    "<img src=\"https://gw.alicdn.com/imgextra/i2/O1CN010zLf411zVoZQ9cWsI_!!6000000006720-2-tps-1592-503.png\" width=\"600\">\n",
    "\n",
    "建立索引是为了将私有知识文档或片段转换为可以高效检索的形式。通过将文件内容分割并转化为多维向量（使用专用 Embedding 模型），并结合向量存储保留文本的语义信息，方便进行相似度计算。向量化使得模型能够高效检索和匹配相关内容，特别是在处理大规模知识库时，显著提高了查询的准确性和响应速度。\n",
    "\n",
    "这些向量经过 Embedding 模型处理后不仅很好地捕捉文本内容的语义信息，而且由于语义已经向量化，标准化，便于之后与检索语义向量进行相关度计算。\n",
    "\n",
    "#### 第二阶段：检索与生成\n",
    "<img src=\"https://img.alicdn.com/imgextra/i1/O1CN01vbkBXC1HQ0SBrC1Ii_!!6000000000751-2-tps-1776-639.png\" width=\"600\">\n",
    "\n",
    "检索生成是根据用户的提问，从索引中检索相关的文档片段，这些片段会与提问一起输入到大模型生成最终的回答。这样大模型就能够回答私有知识问题了。\n",
    "\n",
    "总的来说，基于 RAG 结构的应用，既避免了将整个参考文档作为背景信息输入而导致的各种问题，又通过检索提取出了与问题最相关的部分，从而提高了大模型输出的准确性与相关性。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "## 📝 4. 本节小结\n",
    "在本节课程中，我们学习了以下内容：\n",
    "\n",
    "- **如何使用大模型 API**<br>\n",
    "    通过实际的代码示例，我们掌握了如何通过 API 调用通义千问大模型，并学习了如何通过流式输出来优化用户体验。\n",
    "- **大模型的工作原理与局限性**<br>\n",
    "    我们初步探索了大模型如“黑盒”般的工作流程，理解了其回答具有随机性的原因，并学习了如何通过调整 temperature 和 top_p 等参数来控制生成内容。更重要的是，我们直面了大模型的核心局限：其知识完全依赖于训练数据，无法获知你公司的内部文档等私域知识。\n",
    "- **上下文工程（Context Engineering）与 RAG 的核心思想**\n",
    "    为了解决大模型的知识局限性，我们首先尝试了直接在提示词中“投喂”知识的方法，但很快就遇到了“上下文窗口限制”的瓶颈。这引出了一个至关重要的概念——上下文工程（Context Engineering），即智能地为大模型筛选、组织和提供最相关背景信息的核心方法论。作为该思想的具体实现，我们学习了 RAG（检索增强生成） 技术方案。通过“先检索、后生成”的模式，RAG 能有效地将精准的外部知识与大模型结合，从而在不超出限制的前提下，让模型能够回答私域知识问题。\n",
    "\n",
    "除了本节课程中的示例展示的任务之外，你还可以让大模型完成更多类型的任务，如内容生成、结构化信息提取、文本分类、情感分析等。同时在你的大模型应用中引入 RAG 方案能够扩展大模型所能处理的知识范围。\n",
    "\n",
    "下一小节，我们将动手实践，学习如何创建一个完整的 RAG 应用。\n",
    "\n",
    "\n",
    "### 扩展阅读\n",
    "- 在学习本课程的时候，如果你想了解更多相关概念和原理，可以尝试让大模型进一步展开说明或给出学习建议：\n",
    "> 通义千问支持开启 enable_search 参数，这个参数可以让大模型在生成回答时利用互联网搜索结果来丰富其回复内容。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "completion = client.chat.completions.create(\n",
    "    model=\"qwen-max\",  # 此处以qwen-max为例，可按需更换模型名称。模型列表：https://help.aliyun.com/zh/model-studio/getting-started/models\n",
    "    messages=[\n",
    "        {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n",
    "        {\"role\": \"user\", \"content\": \"巴黎奥运会中国队金牌数\"},\n",
    "    ],\n",
    "    extra_body={\"enable_search\": True},\n",
    ")\n",
    "print(completion.choices[0].message.content)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "阿里云的推理模型 [Qwen3](https://help.aliyun.com/zh/model-studio/deep-thinking) 具有强大的推理能力，模型会先输出思考过程，再输出回答内容。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "reasoning_content = \"\"  # 定义完整思考过程\n",
    "answer_content = \"\"     # 定义完整回复\n",
    "is_answering = False   # 判断是否结束思考过程并开始回复\n",
    "\n",
    "# 创建聊天完成请求\n",
    "completion = client.chat.completions.create(\n",
    "    model=\"qwen3-235b-a22b-thinking-2507\",  # 此处以 qwen3-235b-a22b-thinking-2507 为例，可按需更换模型名称\n",
    "    messages=[\n",
    "        {\"role\": \"user\", \"content\": \"9.9和9.11谁大\"}\n",
    "    ],\n",
    "    stream=True,\n",
    "    # 解除以下注释会在最后一个chunk返回Token使用量\n",
    "    # stream_options={\n",
    "    #     \"include_usage\": True\n",
    "    # }\n",
    ")\n",
    "\n",
    "print(\"\\n\" + \"=\" * 20 + \"思考过程\" + \"=\" * 20 + \"\\n\")\n",
    "\n",
    "for chunk in completion:\n",
    "    # 如果chunk.choices为空，则打印usage\n",
    "    if not chunk.choices:\n",
    "        print(\"\\nUsage:\")\n",
    "        print(chunk.usage)\n",
    "    else:\n",
    "        delta = chunk.choices[0].delta\n",
    "        # 打印思考过程\n",
    "        if hasattr(delta, 'reasoning_content') and delta.reasoning_content != None:\n",
    "            print(delta.reasoning_content, end='', flush=True)\n",
    "            reasoning_content += delta.reasoning_content\n",
    "        else:\n",
    "            # 开始回复\n",
    "            if delta.content != \"\" and is_answering is False:\n",
    "                print(\"\\n\" + \"=\" * 20 + \"完整回复\" + \"=\" * 20 + \"\\n\")\n",
    "                is_answering = True\n",
    "            # 打印回复过程\n",
    "            print(delta.content, end='', flush=True)\n",
    "            answer_content += delta.content"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 如果你对多模态大模型感兴趣，可以参考：\n",
    "\n",
    "    * [视觉理解](https://help.aliyun.com/zh/model-studio/user-guide/vision)\n",
    "    * [音频理解](https://help.aliyun.com/zh/model-studio/user-guide/audio-language-model)\n",
    "    * [全模态](https://help.aliyun.com/zh/model-studio/user-guide/qwen-omni)\n",
    "    * [文生图](https://help.aliyun.com/zh/model-studio/user-guide/text-to-image)\n",
    "    * [视频生成](https://help.aliyun.com/zh/model-studio/user-guide/video-generation)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "## 🔥 课后小测验\n",
    "\n",
    "### 🔍 单选题 2.1.1\n",
    "<details>\n",
    "<summary style=\"cursor: pointer; padding: 12px; border: 1px solid #dee2e6; border-radius: 6px;\">\n",
    "<b>以下代码片段的作用是什么❓</b>\n",
    "<pre style=\"margin: 10px 0;\">\n",
    "os.environ[\"OPENAI_API_KEY\"] = \"your-api-key-here\"\n",
    "</pre>\n",
    "\n",
    "- A. 从磁盘加载API密钥  \n",
    "- B. 将API密钥存储到内存中  \n",
    "- C. 将API密钥设置为环境变量  \n",
    "- D. 创建一个新的 API 密钥  \n",
    "\n",
    "**【点击查看答案】**\n",
    "</summary>\n",
    "\n",
    "<div style=\"margin-top: 10px; padding: 15px; border: 1px solid #dee2e6; border-radius: 0 0 6px 6px;\">\n",
    "\n",
    "✅ **参考答案：C**  \n",
    "📝 **解析**：  \n",
    "该代码通过操作系统的环境变量接口，将API密钥注入到当前运行环境的内存空间中。\n",
    "</div>\n",
    "</details>\n",
    "\n",
    "---\n",
    "\n",
    "### 📝 案例分析题 2.1.2\n",
    "<details>\n",
    "<summary style=\"cursor: pointer; padding: 12px;  border: 1px solid #dee2e6; border-radius: 6px; font-color:#000;\">\n",
    "<b>小明开发写作助手遇到以下两个场景，他应该如何解决问题❓</b> \n",
    "\n",
    "- 场景 🅰️ 生成内容缺乏创意：每次让模型写一篇关于“人工智能发展”的文章时，生成的内容都非常相似。\n",
    "- 场景 🅱️ 生成内容偏离主题：让模型写一份技术文档时，生成的内容经常加入一些不相关的内容。\n",
    "\n",
    "**请问：**\n",
    "1. 基于本节课程中学习的大模型工作流程，这两个场景的问题产生的原因可能是什么？\n",
    "2. 应该如何调整 temperature 或 top_p 参数来解决这些问题？\n",
    "\n",
    "**【点击查看答案】**\n",
    "</summary>\n",
    "\n",
    "\n",
    "<div style=\"margin-top: 10px;  padding: 15px;  border: 1px solid #dee2e6; border-radius: 0 0 6px 6px;\">\n",
    "\n",
    "#### 🎯 场景A解决方案\n",
    "**🔍 原因分析**  \n",
    "`temperature` 值过低（如0.3），导致模型选择单一，生成内容缺乏多样性。\n",
    "\n",
    "**⚙️ 参数调整**  \n",
    "```python\n",
    "temperature = 0.7~0.9  # 提升创意性\n",
    "top_p = 0.9            # 增大选词范围\n",
    "```\n",
    "\n",
    "#### 🎯 场景B解决方案\n",
    "**🔍 原因分析**  \n",
    "`temperature` 过高（如1.2）或 `top_p` 过大\n",
    "\n",
    "**⚙️ 参数调整**  \n",
    "```python\n",
    "temperature = 0.5~0.7  # 降低随机性\n",
    "top_p = 0.7~0.8        # 聚焦高概率词\n",
    "```\n",
    "\n",
    "\n",
    "> 🌟 **调参小技巧**  \n",
    "> 每次调整幅度建议 ±0.2，通过AB测试观察效果变化。\n",
    "> 如果需要兼顾场景A和场景B，推荐组合：`temperature=0.6` + `top_p=0.8`\n",
    "\n",
    "</div>\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ✅评价反馈\n",
    "欢迎你参与[阿里云大模型ACP课程问卷](https://survey.aliyun.com/apps/zhiliao/Mo5O9vuie) 反馈学习体验和课程评价。\n",
    "你的批评和鼓励都是我们前进的动力！"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
